package com.mspbots.core.ticketai.service.impl;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.AtomicDouble;
import com.mspbots.core.AbstractBaseServiceImpl;
import com.mspbots.core.ticketai.TicketController;
import com.mspbots.core.ticketai.mapper.TicketMapper;
import com.mspbots.core.ticketai.model.*;
import com.mspbots.core.ticketai.service.*;
import com.mspbots.core.wise.model.TenantUser;
import com.mspbots.core.wise.service.TenantUserService;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * com.mspbots.core.ticketai.TicketServiceImpl
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2019/8/8
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class TicketServiceImpl extends AbstractBaseServiceImpl<TicketMapper, Ticket> implements TicketService {

    private final AgreementsService agreementsService;
    private final ContactsService contactsService;
    private final TicketRuleService ruleService;
    private final BoardService boardService;
    private final TenantUserService userService;
    private final RedisTemplate<String, Object> redisTemplate;
    private static List<String> filterRuleList = List.of("USER_IN_CAN_NOT_SERVICE_RULE", "HIDE_CHILD_TICKET_RULE","HIDE_CERTAIN_PHASE_RULE","HIDE_SCHEDULE_STATUS_RULE");


    @Override
    public List<Ticket> list(Wrapper<Ticket> queryWrapper) {
        return super.list(queryWrapper);
    }

    @Override
    public Page<Ticket> listForRule(Page<Ticket> page, Ticket ticket) {
        QueryWrapper<Ticket> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("id", "tenant_id", "cw_id", "status", "updated_time", "updated_by");
        queryWrapper.eq("tenant_id", ticket.getTenantId());
        if (!StringUtils.isEmpty(ticket.getStatus())) {
            queryWrapper.eq("status", ticket.getStatus());
        }
        if (ticket.getUpdatedTimeStart() != null) {
            queryWrapper.ge("updated_time", ticket.getUpdatedTimeStart());
        }
        if (ticket.getOwnerNull() != null) {
            queryWrapper.isNotNull("owner");
        }
        return baseMapper.selectPage(page, queryWrapper);
    }


    @Override
    public Ticket selectByCwId(Ticket ticket) {
        QueryWrapper<Ticket> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("cw_id", ticket.getCwId());
        return baseMapper.selectOne(queryWrapper);

    }

    @Override
    public List<TicketStatistic> countEveryday(Long tenantId) {
        QueryWrapper<Ticket> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("tenant_id", tenantId);
        queryWrapper.ge("created_time", LocalDateTime.now().minusDays(7));
        queryWrapper.orderByAsc("created_time");
        List<Ticket> tickets = this.baseMapper.selectList(queryWrapper);
        Map<LocalDate, List<Ticket>> groupMap = tickets.stream().collect(Collectors.groupingBy(item -> item.getCreatedTime().toLocalDate()));
        Iterator iterator = groupMap.keySet().iterator();
        List<TicketStatistic> result = new ArrayList<>();
        while (iterator.hasNext()) {
            LocalDate date = (LocalDate) iterator.next();
            Long completedNum = groupMap.get(date).stream().filter(item -> item.getStatus().toLowerCase().contains("completed")).count();
            result.add(TicketStatistic.builder().createdTime(date).totalNum(groupMap.get(date).size()).completeNum(completedNum.intValue()).build());
        }
        result.sort(Comparator.comparing(TicketStatistic::getCreatedTime));
        return result;
    }

    /**
     * todo get grade ticket list
     * caching is key value is bug
     *
     * @param params get params tenant
     * @return get params ticket all list
     */
    @Override
    public List<Ticket> nextList(TicketController.TicketQueryParams params) {
        Ticket ticket = new Ticket();
        BeanUtils.copyProperties(params, ticket);
        return super.list(Wrappers.lambdaQuery(ticket).orderByDesc(Ticket::getId));
    }

    @Override
    public List<Ticket> getAssignedTicketList(Long tenantId, String ticketIds) {
        List<Long> cwIds = Arrays.stream(StringUtils.commaDelimitedListToStringArray(ticketIds))
                .map(s -> Long.parseLong(s.trim())).collect(Collectors.toList());
        QueryWrapper<Ticket> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("tenant_id", tenantId);
        queryWrapper.in("cw_id", cwIds);
        List<Ticket> tickets = super.baseMapper.selectList(queryWrapper);
        if (!CollectionUtils.isEmpty(tickets)){
            tickets.forEach(t -> t.setDataTotal(tickets.size()));
        }
        return tickets;
    }

    @Override
    public List<TicketOnly> getTicketList(TicketController.TicketQueryParams params) {
        List<TicketOnly> ticketOnlyList = new ArrayList<>();
        Ticket ticket = new Ticket();
        if (!StringUtils.isEmpty(params.getType()) && "next-ticket".equals(params.getType())){
            filterBoardIds(params);
        }
        BeanUtils.copyProperties(params, ticket);
        // select other board assigned tickets
        if (params.getAllAssigned()) {
            Assert.hasText(params.getResources(), () -> "Search Assigned ticket param [resources] not null.");
            ticket.setBoardIds(Arrays.stream(StringUtils.commaDelimitedListToStringArray(params.getBoardAssignedIds()))
                    .map(s -> Long.parseLong(s.trim())).collect(Collectors.toList()));
            ticket.setStatusIds(Arrays.stream(StringUtils.commaDelimitedListToStringArray(params.getStatusAssignedIds()))
                    .map(s -> Long.parseLong(s.trim())).collect(Collectors.toList()));
            List<TicketOnly> ticketAssignedList = baseMapper.getTicketsByTenantIdNew(ticket);
            if (!CollectionUtils.isEmpty(ticketAssignedList)) {
                ticketOnlyList.addAll(ticketAssignedList);
            }
        }
        // select selected board tickets
        if (!StringUtils.isEmpty(params.getBoardIds())) {
            ticket.setResources(null);
            ticket.setBoardIds(Arrays.stream(StringUtils.commaDelimitedListToStringArray(params.getBoardIds()))
                    .map(s -> Long.parseLong(s.trim())).collect(Collectors.toList()));
            ticket.setStatusIds(Arrays.stream(StringUtils.commaDelimitedListToStringArray(params.getStatusIds()))
                    .map(s -> Long.parseLong(s.trim())).collect(Collectors.toList()));
            List<TicketOnly> ticketSelectList = baseMapper.getTicketsByTenantIdNew(ticket);
            if (!CollectionUtils.isEmpty(ticketSelectList)) {
                ticketOnlyList.addAll(ticketSelectList);
            }
        }
        // filter rejected tickets
        if (!StringUtils.isEmpty(params.getRejectTicketIds()) && !CollectionUtils.isEmpty(ticketOnlyList)) {
            List<Long> rejectTickets = Arrays.stream(StringUtils.commaDelimitedListToStringArray(params.getRejectTicketIds()))
                    .map(s -> Long.parseLong(s.trim())).collect(Collectors.toList());
            ticketOnlyList = ticketOnlyList.stream().filter(ticketOnly -> !rejectTickets.contains(ticketOnly.getCwId()))
                    .collect(Collectors.toList());
        }
        filterTicketByRule(ticketOnlyList, params);
        int dataTotal = ticketOnlyList.size();
        if (!CollectionUtils.isEmpty(ticketOnlyList)) {
            List<Agreements> agreementsList = agreementsService.list(Wrappers.<Agreements>lambdaQuery()
                    .eq(Agreements::getTenantId, ticket.getTenantId())
                    .select(Agreements::getCompanyId, Agreements::getBillAmount));
            List<Contacts> contactsList = contactsService.list(Wrappers.<Contacts>lambdaQuery()
                    .eq(Contacts::getTenantId, ticket.getTenantId())
                    .select(Contacts::getCwId, Contacts::getTitle));

            ticketOnlyList.parallelStream().forEach(t -> {
                contactsList.parallelStream().filter(c -> t.getContactId().equals(c.getCwId())).findAny()
                        .ifPresent(contacts -> t.setContactTitle(contacts.getTitle()));

                AtomicDouble amount = new AtomicDouble();
                agreementsList.parallelStream().filter(a -> t.getCompanyId().equals(a.getCompanyId()) && a.getBillAmount() != null)
                        .forEach(a ->amount.addAndGet(a.getBillAmount()));
                t.setBillAmount(amount.get() == 0.0 ? null : amount.get());
                if (!ObjectUtils.isEmpty(t.getCompanyExtend()) && !ObjectUtils.isEmpty(t.getCompanyExtend().get("status"))){
                    t.setCompanyStatusId(t.getCompanyExtend().get("status").get("id").asLong());
                    t.setCompanyStatusName(t.getCompanyExtend().get("status").get("name").asText());
                }
                t.setDataTotal(dataTotal);
            });
        }
        return ticketOnlyList;
    }

    private void filterBoardIds(TicketController.TicketQueryParams params) {
        if (ObjectUtils.isEmpty(params.getResources())) {
            return;
        }
        List<BoardOnly> boardList = boardService.getListOnly(params.getTenantId(), null);
        Set<Long> excludedboardIds = new HashSet<>();

        TenantUser user = userService.getOne(Wrappers.<TenantUser>lambdaQuery()
                .eq(TenantUser::getTenantId, params.getTenantId())
                .eq(TenantUser::getIdentifier, params.getResources()).last("limit 1"));
        if (ObjectUtils.isEmpty(user)){
            return;
        }
        boardList.stream().filter(board -> !ObjectUtils.isEmpty(board.getExcludedMembers())).forEach(board -> {
            Iterator<JsonNode> elements = board.getExcludedMembers().elements();
            while (elements.hasNext()) {
                JsonNode node = elements.next();
                if (user.getCwUid().equals(node.get("memberId").asLong())) {
                    excludedboardIds.add(board.getCwId());
                    break;
                }
            }
        });
        if (CollectionUtils.isEmpty(excludedboardIds)) {
            return;
        }
        if (!StringUtils.isEmpty(params.getBoardIds())) {
            List<Long> boardIds = Arrays.stream(StringUtils.commaDelimitedListToStringArray(params.getBoardIds()))
                    .map(s -> Long.parseLong(s.trim())).collect(Collectors.toList());
            Iterator<Long> boardIdIterator = boardIds.iterator();
            while (boardIdIterator.hasNext()) {
                if (excludedboardIds.contains(boardIdIterator.next())) {
                    boardIdIterator.remove();
                }
            }
            params.setBoardIds(StringUtils.collectionToCommaDelimitedString(boardIds));
        }
        if (!StringUtils.isEmpty(params.getBoardAssignedIds())) {
            List<Long> boardAssignedIds = Arrays.stream(StringUtils.commaDelimitedListToStringArray(params.getBoardAssignedIds()))
                    .map(s -> Long.parseLong(s.trim())).collect(Collectors.toList());
            Iterator<Long> boardAssignedIdIterator = boardAssignedIds.iterator();
            while (boardAssignedIdIterator.hasNext()) {
                if (excludedboardIds.contains(boardAssignedIdIterator.next())) {
                    boardAssignedIdIterator.remove();
                }
            }
            params.setBoardAssignedIds(StringUtils.collectionToCommaDelimitedString(boardAssignedIds));
        }

    }

    private void filterTicketByRule(List<TicketOnly> ticketOnlyList, TicketController.TicketQueryParams queryQarams) {
        if (CollectionUtils.isEmpty(ticketOnlyList)) {
            return;
        }
        for (String ruleName : filterRuleList) {
            List<TicketRule> ruleList = ruleService.getRules(queryQarams.getTenantId(), "", ruleName);
            if (CollectionUtils.isEmpty(ruleList)) {
                continue;
            }
            Map<Integer, TicketRule> ruleMap = ruleList.stream().collect(Collectors.toMap(TicketRule::getBoard, Function.identity(), (key1, key2) -> key2));

            Iterator<TicketOnly> iterator = ticketOnlyList.iterator();
            while (iterator.hasNext()) {
                TicketOnly ticket = iterator.next();
                TicketRule rule = ruleMap.get(ticket.getBoardId().intValue());
                if (ObjectUtils.isEmpty(rule)) {
                    continue;
                }
                boolean flag = false;
                switch (ruleName) {
                    case "USER_IN_CAN_NOT_SERVICE_RULE":
                        flag = userInCanNotServiceRuleProcess(queryQarams.getResources(), ticket, rule);
                        break;
                    case "HIDE_CHILD_TICKET_RULE":
                        flag = hideChildTicketRuleProcess(ticket, rule);
                        break;
                    case "HIDE_CERTAIN_PHASE_RULE":
                        flag = hideCertainPhaseRuleProcess(ticket, rule);
                        break;
                    case "HIDE_SCHEDULE_STATUS_RULE":
                        flag = hideScheduleStatusRuleProcess(queryQarams.getResources(),ticket, rule);
                        break;
                    default:
                        break;
                }
                if (flag) {
                    iterator.remove();
                }
            }
        }
    }

    private Boolean userInCanNotServiceRuleProcess(String identifier, TicketOnly ticket, TicketRule rule) {
        if (StringUtils.isEmpty(identifier) || ObjectUtils.isEmpty(ticket.getTeamRoles()) || ticket.getTeamRoles().size() == 0) {
            return false;
        }

        List<UserInCanNotServiceParam> params = this.objectMapper.convertValue(rule.getParams(), new TypeReference<>() {
        });

        List<TeamRoles> teamRolesList = new ArrayList<>();
        Iterator<JsonNode> nodeJson = ticket.getTeamRoles().elements();
        while (nodeJson.hasNext()) {
            ObjectNode objectNode = (ObjectNode) nodeJson.next();
            if (!StringUtils.isEmpty(objectNode.get("teamRole")) && !StringUtils.isEmpty(objectNode.get("member"))) {
                teamRolesList.add(TeamRoles.builder().teamRoleId(objectNode.get("teamRole").get("id").asInt())
                        .memberIdentifier(objectNode.get("member").get("identifier").asText()).build());
            }
        }
        UserInCanNotServiceParam p = params.get(0);
        if (!p.isSelected()) {
            return false;
        }
        for (TeamRoles t : teamRolesList) {
            if (p.getTeamRoleIds().contains(t.teamRoleId) && identifier.equals(t.getMemberIdentifier())) {
                return true;
            }
        }
        return false;
    }

    private Boolean hideChildTicketRuleProcess(TicketOnly ticket, TicketRule rule) {
        if (ObjectUtils.isEmpty(ticket.getExtend().get("parentTicketId"))) {
            return false;
        }

        List<HideTicketParam> params = this.objectMapper.convertValue(rule.getParams(), new TypeReference<>() {
        });
        HideTicketParam p = params.get(0);
        if (p.isSelected()) {
            return true;
        }
        return false;
    }
    private Boolean hideCertainPhaseRuleProcess(TicketOnly ticket, TicketRule rule) {
        if (ObjectUtils.isEmpty(ticket.getExtend().get("phase"))){
            return false;
        }
        Integer phaseId = ticket.getExtend().get("phase").get("id").asInt();

        List<HideCertainPhaseParam> params = this.objectMapper.convertValue(rule.getParams(), new TypeReference<>() {
        });
        HideCertainPhaseParam p = params.get(0);
        if (!p.isSelected()) {
            return false;
        }
        if (p.getPhaseIds().contains(phaseId)){
            return true;
        }
        return false;
    }
    private Boolean hideScheduleStatusRuleProcess(String identifier, TicketOnly ticket, TicketRule rule) {
        if (StringUtils.isEmpty(identifier) || ObjectUtils.isEmpty(ticket.getStatus()) || !ticket.getStatus().contains("schedule")) {
            return false;
        }
        List<HideTicketParam> params = this.objectMapper.convertValue(rule.getParams(), new TypeReference<>() {
        });
        HideTicketParam p = params.get(0);
        if (!p.isSelected()) {
            return false;
        }
        return false;
    }

    @Override
    public Integer deleteBatch(TicketController.TicketQueryParams params) {
        return this.baseMapper.delete(Wrappers.<Ticket>lambdaQuery()
                .eq(Ticket::getTenantId, params.getTenantId())
                .in(!StringUtils.isEmpty(params.getStatusIds()), Ticket::getStatusId,
                        Arrays.stream(StringUtils.commaDelimitedListToStringArray(params.getStatusIds()))
                                .map(s -> Long.parseLong(s.trim())).collect(Collectors.toList())));
    }

    @Override
    public Ticket findNextTicketRate(Ticket queryParams) {
        if(!StringUtils.isEmpty(queryParams.getTeam())) {
            List<String> teamList = new ArrayList<>();
            if (queryParams.getTeam().contains(",")) {
                teamList = Arrays.asList(queryParams.getTeam().split(","));
            } else {
                teamList.add(queryParams.getTeam());
            }
            queryParams.setTeamList(teamList);
        }
        return this.baseMapper.findNextTicketRate(queryParams);
    }

    private static final String REDIS_KEY_PREFIX_USER = "mspbots:users:bot:";

    @Override
    public Object setCache(BotTicketCache queryParams) {
        String cacheKey = REDIS_KEY_PREFIX_USER + queryParams.getUserId() + ":" + queryParams.getScriptId();
        Long num = 0L;
        if (StringUtils.isEmpty(redisTemplate.opsForValue().get(cacheKey))) {
            redisTemplate.opsForValue().set(cacheKey, num + 1);
        } else {
            Object cache = redisTemplate.opsForValue().get(cacheKey);
            redisTemplate.opsForValue().set(cacheKey, Long.valueOf(String.valueOf(cache)) + 1);
        }
        return true;
    }

    @Override
    public Boolean removeCache(BotTicketCache queryParams) {
        String cacheKey = REDIS_KEY_PREFIX_USER + queryParams.getUserId() + ":" + queryParams.getScriptId();
        Set<String> keys = redisTemplate.keys(cacheKey);
        return redisTemplate.delete(keys) > 0;
    }

    @Override
    public Object getCache(BotTicketCache queryParams) {
        String cacheKey = REDIS_KEY_PREFIX_USER + queryParams.getUserId() + ":" + queryParams.getScriptId();
        Object cache = redisTemplate.opsForValue().get(cacheKey);
        return Map.of("num", cache);
    }

    @Data
    private static class UserInCanNotServiceParam {
        private Integer index;
        private List<Integer> teamRoleIds;
        private boolean selected;
    }

    @Data
    @Builder
    private static class TeamRoles {
        private int teamRoleId;
        private String memberIdentifier;
    }

    @Data
    private static class HideTicketParam {
        private Integer index;
        private String condition;
        private boolean selected;
    }

    @Data
    private static class HideCertainPhaseParam {
        private Integer index;
        private List<Integer> phaseIds;
        private boolean selected;
    }
}
